import {
  Directive,
  Input,
  ElementRef,
  ViewContainerRef,
  ComponentFactory,
  ComponentFactoryResolver,
  SimpleChanges,
  EventEmitter,
  Output,
  TemplateRef,
  Renderer2,
  forwardRef,
  OnChanges,
  OnDestroy,
  OnInit,
  TrackByFunction,
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { DOWN_ARROW, ENTER, ESCAPE, UP_ARROW } from '@angular/cdk/keycodes';
import { Subject, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { LvSizeLD } from '../core/types';
import { InputBoolean, InputNumber } from '../core/utils/convert';
import { AutocompleteComponent } from './autocomplete.component';
import { AutocompleteItem } from './interface';

@Directive({
  selector:
    'input[lv-autocomplete], input[lvAutocomplete], textarea[lv-autocomplete], textarea[lvAutocomplete], [lv-autocomplete]',
  exportAs: 'lvAutocomplete',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef((): typeof AutocompleteDirective => AutocompleteDirective),
      multi: true,
    },
  ],
  host: {
    autocomplete: 'off',
    '(click)': '_doOpen()',
    '(focusin)': '_doOpen()',
    '(input)': '_doInput($event)',
    '(keydown)': '_doKeydown($event)',
  },
})
export class AutocompleteDirective implements ControlValueAccessor, OnInit, OnChanges, OnDestroy {
  @Input('lvAutocompleteWidth') lvWidth: string | number;

  @Input('lvAutocompletePanelClass') lvPanelClass: string;

  @Input('lvAutocompleteSize') lvSize: LvSizeLD = 'default';

  @Input('lvAutocompleteLoading') lvLoading: boolean = false;

  @Input('lvAutocompleteHeader') lvHeader: TemplateRef<void>;

  @Input('lvAutocompleteOrigin') lvOrigin: ElementRef = this.el;

  @Input('lv-autocomplete') @InputBoolean() lvAutocomplete = true; // 是否执行

  @Input('lvAutocompleteShowNoData') lvShowNoData: boolean = true;

  @Input('lvAutocompleteTrackBy') lvTrackBy: TrackByFunction<any>; // 内容列表trackBy

  @Input('lvAutocompleteHighlightContent') lvHighlightContent: string;

  @Input('lvAutocompleteData') lvData: string[] | AutocompleteItem[] = [];

  @Input('lvAutocompleteLabelTemplate') lvLabelTemplate: TemplateRef<void>;

  @Input('lvAutocompleteVirtualItemSize') @InputNumber() lvVirtualItemSize; // 每行高度

  @Output('lvAutocompleteScrollEnd') lvScrollEnd = new EventEmitter<any>(); // 虚拟滚动到底部时回调事件

  @Input('lvAutocompleteNoDataTemplate') lvNoDataTemplate: TemplateRef<void>;
  @Input('lvAutocompleteVirtualMaxBuffer') @InputNumber() lvVirtualMaxBuffer; // 渲染新项时最大缓冲高度
  @Input('lvAutocompleteVirtualMinBuffer') @InputNumber() lvVirtualMinBuffer; // 视图之外渲染的最小缓冲区数，若缓存区数量低于此数量将渲染新项
  @Output('lvAutocompleteShow') lvShow: EventEmitter<any> = new EventEmitter();
  @Output('lvAutocompleteHide') lvHide: EventEmitter<any> = new EventEmitter();
  @Input('lvAutocompleteVirtualScroll') @InputBoolean() lvVirtualScroll = false; // 是否使用虚拟滚动（大数据场景使用）

  @Input('lvAutocompleteScroll') lvScroll: { x?: string | null; y?: string | null }; // 下拉选项水平垂直高度，超过滚动 目前只有y

  @Output('lvAutocompleteScrollingChange') lvScrollingChange = new EventEmitter<any>(); // 虚拟滚动中回调事件

  @Output('lvAutocompleteEnterEvent') lvEnterEvent: EventEmitter<any> = new EventEmitter();

  @Output('lvAutocompleteInputChange') lvInputChange: EventEmitter<any> = new EventEmitter();

  @Output('lvAutocompleteSelectedChange') lvSelectedChange: EventEmitter<any> = new EventEmitter();

  @Input('lvAutocompletePosition') lvPosition:
    | 'topLeft'
    | 'topCenter'
    | 'topRight'
    | 'bottomLeft'
    | 'bottomCenter'
    | 'bottomRight' = 'bottomLeft';

  public inputChange$ = new Subject<any>();

  public autocomplete: AutocompleteComponent;
  private selectionSubscription: Subscription;
  private defaultVirtualItemSizeConfig = {
    large: 40,
    default: 32,
    small: 24,
  };

  private ComponentFactory: ComponentFactory<AutocompleteComponent> =
    this.resolver.resolveComponentFactory(AutocompleteComponent);

  private properties = [
    'lvData',
    'lvWidth',
    'lvLoading',
    'lvShowNoData',
    'lvNoDataTemplate',
    'lvLabelTemplate',
    'lvPanelClass',
    'lvPosition',
    'lvShow',
    'lvHide',
    'lvHeader',
    'lvHighlightContent',
    'lvSize',
    'lvVirtualScroll',
    'lvVirtualItemSize',
    'lvVirtualMaxBuffer',
    'lvVirtualMinBuffer',
    'lvScroll',
    'lvScrollEnd',
    'lvScrollingChange',
    'lvTrackBy',
  ];

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private resolver: ComponentFactoryResolver,
    private ViewContainer: ViewContainerRef,
  ) {}

  public _onTouch: any = (): void => {};

  public _onChange: any = (): void => {};

  registerOnChange(fn: any): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this._onTouch = fn;
  }

  _close(): void {
    this.autocomplete.dropopen$.next(false);
  }

  writeValue(value: any): void {
    this._setCurrentValue(value);
  }

  ngOnDestroy(): void {
    this.selectionSubscription.unsubscribe();
  }

  _doOpen(): void {
    if (this.isEnabled() && this.lvData.length > 0) {
      this._open();
    }
  }

  _setCurrentValue(value): void {
    this.el.nativeElement.value = value || '';
    this.autocomplete.selected = value;
    this.autocomplete.active = value;
    this.autocomplete.inputValue = value || '';
  }

  _open(): void {
    if (!this.lvAutocomplete) {
      return;
    }
    this.autocomplete.target = this.lvOrigin;
    this.autocomplete.panelWidth = this.lvOrigin.nativeElement.offsetWidth;
    this.autocomplete.dropopen$.next(true);
  }

  _doInput(event: KeyboardEvent): void {
    const target = event.target as HTMLInputElement;
    let val: number | string | null = target.value;

    if (this.isEnabled()) {
      if (target.type === 'number') {
        val = val === '' ? null : parseFloat(val);
      }
      this._setCurrentValue(val);
      this._onChange(val);
      this.inputChange$.next(val);
    }
  }

  ngOnChanges(change: SimpleChanges): void {
    if (change.lvData) {
      if (change.lvData.currentValue === undefined) {
        this.lvData = [];
      }
    }
    const flag = change.lvData || change.lvWidth || change.lvLoading || change.lvHighlightContent;
    if (flag) {
      this.lvVirtualItemSize = this.lvVirtualItemSize || this.defaultVirtualItemSizeConfig[this.lvSize];
      this.initAutoCompleteInstance();
      // Update property to component
      this.properties.forEach((property): void => {
        this.updateComponentProperties(property, this[property]);
      });
      this.autocomplete?.genScrollY();
      this.autocomplete?.genVirtualScrollY();
      this.autocomplete?.cdr.markForCheck();
    }
  }

  ngOnInit(): void {
    this.lvVirtualItemSize = this.lvVirtualItemSize || this.defaultVirtualItemSizeConfig[this.lvSize];
    this.initAutoCompleteInstance();

    // Update property to component
    this.properties.forEach((property): void => {
      this.updateComponentProperties(property, this[property]);
    });

    this.selectionSubscription = this.autocomplete.valueChange$.subscribe((res): void => {
      const data = res.value || res;
      this._setCurrentValue(data);
      this._onChange(data);
      this.lvSelectedChange.emit(res);
      this.el.nativeElement.focus();
      this._close();
    });

    // Control the triggering frequency
    this.inputChange$.pipe(debounceTime(300)).subscribe((value): void => {
      if (!this.isEnabled()) {
        return;
      }
      this.autocomplete._scrollViewport?.scrollToIndex(0);
      this._open();
      this.lvInputChange.emit(value);
    });
  }

  _doKeydown(event: KeyboardEvent): void {
    if (!this.lvAutocomplete) {
      return;
    }
    // tslint:disable-next-line:deprecation
    const { keyCode } = event;
    const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;

    if (this.autocomplete.isPanelOpen) {
      if (keyCode === ESCAPE) {
        this._close();
        event.preventDefault();
      } else if (keyCode === ENTER) {
        this.handleEnter();
        event.preventDefault();
      } else if (isArrowKey) {
        if (keyCode === UP_ARROW) {
          this.setSelectionItem(true);
        } else {
          this.setSelectionItem(false);
        }

        event.stopPropagation();
        event.preventDefault();
      }
    } else if (keyCode === DOWN_ARROW) {
      this._doOpen();
      event.stopPropagation();
      event.preventDefault();
    } else if (keyCode === ENTER) {
      this.lvEnterEvent.emit(this.el.nativeElement.value);
    }
  }

  private handleEnter(): void {
    this._setCurrentValue(this.autocomplete.active);
    this._onChange(this.autocomplete.active);
    if (this.isSelected()) {
      const data = (this.lvData as Array<any>).find((item): boolean => {
        if (item.value) {
          return item.value === this.autocomplete.selected;
        } else {
          return item === this.autocomplete.selected;
        }
      });
      this.lvSelectedChange.emit(data);
      this._close();
    } else {
      this.lvEnterEvent.emit(this.el.nativeElement.value);
    }
  }

  private isEnabled(): boolean {
    const element: HTMLInputElement = this.el.nativeElement;
    return !element.readOnly && !element.disabled;
  }

  private updateComponentProperties(key, value): void {
    if (this.autocomplete && (value !== null || value !== undefined)) {
      this.autocomplete[key] = value;
    }
  }

  private isActive(): boolean {
    return (
      this.autocomplete.active !== '' &&
      this.lvData.some((item): boolean => this.autocomplete.active === (item.value || item))
    );
  }

  private isSelected(): boolean {
    return (
      this.autocomplete.selected !== '' &&
      this.lvData.some((item): boolean => this.autocomplete.selected === (item.value || item))
    );
  }

  private initAutoCompleteInstance(): void {
    if (!this.autocomplete) {
      const componentRef = this.ViewContainer.createComponent(this.ComponentFactory);
      this.autocomplete = componentRef.instance;
      // Remove selector element
      this.renderer.removeChild(this.renderer.parentNode(this.el.nativeElement), componentRef.location.nativeElement);
    }
  }

  private setSelectionItem(moveup): void {
    let value;
    let item;
    if (this.isActive()) {
      let index = this.lvData.findIndex(
        (option): boolean => ((option as AutocompleteItem).value || (option as string)) === this.autocomplete.active,
      );
      if (moveup) {
        index--;
        index < 0 && (index = this.lvData.length - 1);
      } else {
        index++;
        index > this.lvData.length - 1 && (index = 0);
      }
      item = this.lvData[index];
      value = (item as AutocompleteItem).value || (item as string);
    } else {
      if (this.lvData.length === 0) {
        return;
      }
      item = this.lvData[0];
      if (moveup) {
        item = this.lvData[this.lvData.length - 1];
      }
      value = (item as AutocompleteItem).value || (item as string);
    }

    this.autocomplete.active = value;

    // 设置选中项在列表中可见
    const delay = setTimeout((): void => {
      this.autocomplete.setSelectedItemPosition();
      clearTimeout(delay);
    }, 0);
  }
}
